跨桌面端Web容器演进
浏览器是大家所熟悉的。本文会从容器的架构演进,容器的功能增强和稳定性保障及未来的演进,这3个方面介绍一下我们的应用程序是如何对接浏览器的能力的。有问题的地方欢迎大家指正,也欢迎对浏览器内核感兴趣的同学一起探索。
此文为本系列文章第二篇,
第一篇:跨全端SDK技术演进
本文:跨桌面端之Web容器演进
第三篇:跨桌面端之组件化实践
随着客户端技术的逐渐发展,Web容器已经深入客户端,为客户端构建了丰富多彩的页面和交互体验,成为客户端的一个不可或缺的重要组成部分。在千牛客户端上,Web容器不仅承载了千牛的开放容器(经历了H5插件到千牛小程序的变革),而且一些核心功能,比如聊天窗口,消息中心,也都是由Web容器承载的。
一开始,Web容器在不同的操作系统有各自的选择,比如在Widnows上的IE内核,而在Macos用WKWebview。但是随着Chrome的发展,渐渐的Chromium开始一统江湖。大家也开始基于开源的Chromium定制自己的内核。自己定制的内核不仅有着跨端的特性,而且能很好的支持前端的业务诉求。千牛也在此时定制了自己的web容器AEF,全称Application Embedded Framework。
▐ Application Embedded Framework(AEF)
AEF是千牛自己是基于chromium源码定制的内核。
AEF的框架结构
概括来看AEF框架主要封装了如下功能
控件接口,暴露接口给PC客户端,比如支持浏览网页,前进,后退,刷新,强制刷新,中止等功能
事件响应,AEF上面开放了很多事件出来,供应用来决定是否处理相应的事件,比如文档加载完成,页面加载失败,资源加载失败,页面发生崩溃,标题发生刷新,url发生重定向等。
JS和本地C++的交互,这个是通过render delegate直接向WebKit注册native sdk的dll,再通过事件发送到PC客户端完成的。
丰富的扩展功能,比如定制自定义协议alires,完成本地包的加载和支持打印等功能。
▐ Chromium Embedded Framework (CEF)
早期AEF已经能很好的满足我们的需求。但随着开源技术的发展。CEF已经涵盖了AEF大部分的功能。同时为了解决安全漏洞,防止被灰产利用,为了更好跟进前端技术的发展,我们有了升级内核的需求。因为Chromium变化巨大,如果在AEF的基础上升级内核,我们不仅需要基于Chromium重新构建嵌入式框架,而且需要将原有功能重新实现一遍。而CEF是由 Marshall Greenblatt 于 2008 年创立的基于BSD 许可的开源项目,CEF 专注于促进第三方应用程序中嵌入浏览器框架,使用户免受底层 Chromium 和 Blink 代码复杂性的影响。所以如果我们选择CEF,可以免去一些基本的功能开发,而将更多的关注放在定制功能的开放上,从而提高我们的研发效率。
▐ 容器框架切换
于是,我们决定将Web容器框架从AEF升级到CEF,并且在千牛上做了相应的改造。以其能平滑的迁移到CEF框架之上。Chromium本身是多进程架构的,Chromium为了保护应用程序不受某个页面的影响,将js引擎和渲染部分都独立到了Render进程。而且在新的内核中,网络,本地存储都隔离到Render进程。而Browser则对应应用程序的UI管理进程。在通常的CEF框架应用程序中,应用程序即担负起Browser进程的责任,应用程序整个UI消息泵由CEF托管。这也是合情合理的,因为Browser本身就是UI的管理进程,要响应窗口的创建销毁事件,所以,一开始,千牛是包含了Chromium的主进程的。但是,正如Chromium架构本身,Render的崩溃会影响应用程序的稳定性,Browser的异常一会影响到千牛的稳定性。为此,我们决定将Browser进程从千牛进程中剥离,从而保证千牛进程的独立性。在Browser崩溃时,我们还做了守护,保障Browser进程在崩溃后能自行恢复。
千牛的多进程架构
Browser进程隔离的前提是操作系统支持父子窗口能够跨进程,千牛中是由webview容器决定窗口的位置,并将句柄告知浏览器进程。并且Windows系统下,父子窗口可以在不同的进程中通信,但MacOS系统限制了这一点。为此我们需要一种灵活的方式,使得 Windows系统下Browser进程能独立于千牛进程,而MacOS下千牛和Browser则是一体的。为此,我们借鉴Chromium内核的mojo通信,设计了一套RPC通信框架,用于千牛和Browser之间的通信。又借助组件化编程的思维,设计了远程组件的概念。让工程可以想搭积木式的选择自己的架构。
▐ 远程组件
组件,大家都知道,它是模块化编程的一种方式,有着高内聚低耦合的特点。利用它可以实现搭积木式的编程。我们的组件也是学习了Microsoft的COM技术来实现的。那么远程组件是什么概念呢?远程组件也是组件,但它不提供真正的服务,一般情况下需要通过RPC连接到真正的组件上,通过RPC调用进而提供服务。使用者不关心自己调用的组件是远程组件还是真正提供服务的组件。而工程化时只需要通过搭积木的拼接部署,就可以实现产品的架构调整。我们这里在千牛中,就是通过这种方式实现的。
远程组件的实现方式
熟悉COM的人都知道,组件中有一个ClassID的概念,可以叫他类ID,意思一套组件化接口可以有多种实现。我们这里WebViewRemote和WebViewLocal实际上都是WebView,只是一个ClassID是Remote,另一个是Local。Remote即远程组件,而Local则是真正提供服务的组件。后续加入我们能提供多种进程间通信方式的话,我们的ClassID还可以根据通信方式来扩展,比如zmq,mojo等。目前我们只实现了一套基于Chromium IPC的进程间通信方式,在IPC通信的基础上,我们加上了rapid_json的序列化方式。封装到了RPCFramework中,为我们的远程组件提供通信支撑。
▐ 全新JS注入方式
不管是AEF和CEF,JS的注入和本地C++的通信都是业务的一个重要基点。但从结构中我们看到js的注入和c++的交互代码通常会封装在一个dll插入Render进程中,而业务通常在Aliworkbench千牛进程中。中间需要Browser进程进行事件的转发作为通信的桥梁。为此增加一个js注入节点需要3个不同地方实现相应代码,开发十分不方便。在新的WebView内部,我们提供了一个灵活的接口,在接口内,通过远程组件对通信进行了封装,并且自动创建的js节点注入的代码。使调用者更多的关注业务的本身,而无需关心内部注入流程、js和c++交互流程和序列化相关等这些繁琐的步骤。
新的接口不再需要通过dll注入js节点
▐ Render分组优化
在Browser进程隔离完成后,我们对Render进程也做了分组优化。在之前的Chromiun的多进程图中,我们知道Chromium会根据页面的不同,创建多个Render进程。Chromium的Render进程分组策略是由站点的Origin决定的,Origin是url的scheme和host的组合,不同的Origin对应Siteinstance不同,进而对应的Render进程就会不同。但这种策略在千牛中却显的不是很合适。因为多个Render进程势必会照成一些不必要的开销和内存的浪费。为此我们设计了一种新的策略,为Render分组,在创建WebView的时候即指定RenderID, 在此Webview上的的页面和子iframe都会在RenderID对应的Render进程中运行。基于这个特性,我们将千牛聊天记录页面放到一个专属的Render进程中,千牛的二方页面放在一个独立的Render进程,还有三方页面,插件页面,小程序页面等都进行了分组。这样做既节省内存开销,同时保证了核心页面的稳定性,还大大额方便了问题定位和调试。
Render分组
▐ 网络收口,打造安全容器
近年来,黑灰产日益猖獗,对我们的容器安全性带来了一定的挑战。因为H5过于开放的模式,导致我们的数据被泄露。为此我们决定管控我们端侧的容器的所有网络请求,包括WebSocket和XMLhttpReqest。而原生的Chromium内核并未开放拦截的卡口,所以CEF也并未支持WebSocket的拦截,仅支持网络资源的请求的拦截。为此我们打算通过定制内核,提供统一的网络收口,并联合白名单机制,打造一个安全的web跨端容器。并且为了支持二次开放等多样化的拦截需求,我们还支持ifame级别的拦截,使用方可以根据iframe的id来做区分。
网络拦截的内部流程
▐ DNS解析拦截,支持AMDC
DNS在互联网中的一个很重要的服务,dns解析出现异常,任务服务再强大也无法访问。历史上也出现了很多因为DNS异常导致的故障。为了重点保障我们的核心页面,我们希望能够在DNS异常的情况下,仍然能够证确的访问目标。为此我们拦截的内核的DNS解析流程,通过AMDC的动态配置,保障我们的核心域名稳定。通过引入AMDC HTTPDNS方案 整体千牛mtop网关成功率从99.6%提升到99.8%。
DNS解析拦截流程
▐ HTML Node节点打印功能
千牛打单可谓是千牛商家非常常用的功能了,因为发货单或者快递单的特殊性,客户往往不需要打印整个页面。开发者需要打印当前页面的一个html node节点,比如某个iframe,或者某个div。但原生的chromium支持整页打印的。我们需要为小程序提供node节点打印,为此我们深入定制了内核打印接口。
CefClient内的打印流程
▐ iframe增加appid自定义属性
PC小程序中的LocalWebview是利用iframe实现的,为了能够让不同的localWebView能证确的路由到小程序包,我们在iframe的标签中增加了自定义属性appid的解析。千牛端在识别appid后可以把资源获取路由到对应的小程序包中。小程序利用这一点支持了插件webview功能,从而实现了丰富的二次开放。
▐ UserAgent增强
原生的UserAgent是跟随固定名称如Chrome/98.0.4758.102,一个应用程序中,UA是固定的,不允许随意更改。但我们提供WebView容器级别的UA定制。即同在千牛中,不同的WebView的UserAgent 可以增加自定义字段,从而实现在H5插件的WebView和小程序的WebView可以展现不同的UA。利用者一点,ISV可以实现一套代码同时应用在插件和小程序上,完成自己特定的功能。
▐ 清理浏览器缓存
浏览器的缓存大大提升了浏览器加载速度,但是偶儿因为自身或服务端的异常,会导致浏览器缓存异常。我们通过在CEF中增加接口,沿袭Chromium内部清理缓存流程,实现了可以在不退出应用程序的情况完成缓存的清理。使缓存出现异常的情况下能够恢复。
▐ 更好的内存回收策略
CEF的框架下,我们沿用了AEF时期的内存回收策略,更多的是在业务测进行。比如在判断用户空闲时对于不活动的WebControl进行回收。在判断单Render进程内存超过一定阈值时重新启动相应Render进程。但这些都会损失一定的用户体验。所以我们打算借鉴钉钉之前研究过的Tab休眠策略,让不活动的页面仅保留缩略图,从而在不损失用户体验的情况下,实现内存的优化。
▐ 白屏检测
之前我们做过相应的白屏检测的研究。有区于依赖前端技术的白屏检测,客户端做的检测会更贴近用户真实体验。因为前端只能通过检测DOM树的变化来感知,但是否在真的渲染以及真实呈现在用户视觉上的东西是无法感知的。我们通过检测Render最后生成的渲染帧的绘制指令,以及观察最后窗口的贴图动作能最接近用户测的感知到是否白屏。但在之前预研中,我用统计出来的白屏却高于正常值,需要进一步调研,也欢迎有兴趣的同学一起探索。
我们是大淘宝技术部行业与商家技术跨终端技术团队,业务上负责为千万级商家打造最高效的一站式工作台千牛,为淘宝上亿商家和消费者提供稳定高效的端到端消息IM服务;技术上深耕C++跨终端及PC桌面端技术(Windows&Mac),为商家,消费者提供稳定,可靠,高效的客户端产品。